package cn.admin.config.common.lang.enumeration;

import cn.admin.config.common.lang.ClassLoaderUtil;
import cn.admin.config.common.lang.StringUtil;
import cn.admin.config.common.lang.enumeration.internal.NumberType;

import java.io.InvalidClassException;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.text.MessageFormat;
import java.util.*;

public abstract class Enum extends Number implements NumberType, Comparable, Serializable {

    private String name;
    private Number value;

    protected Enum() {

    }

    public String getName() {
        if (this.name == null) {
            Class enumClass = this.ensureClassLoaded();
            Enum.EnumType enumType = EnumUtil.getEnumType(enumClass);
            enumType.populateNames(enumClass);
        }

        return this.name;
    }

    public Number getValue() {
        return value;
    }

    @Override
    public byte byteValue() {
        return (byte) this.intValue();
    }

    @Override
    public short shortValue() {
        return (short) this.intValue();
    }

    @Override
    @SuppressWarnings("unchecked")
    public int compareTo(Object otherEnum) {
        if (!this.getClass().equals(otherEnum.getClass())) {
            throw new CreateEnumException(MessageFormat.
                    format("Could not compare object of \"{0}\" with object of \"{1}\"",
                            this.getClass().getName(), otherEnum.getClass().getName()));
        } else {
            return ((Comparable) this.value).compareTo(((Enum) otherEnum).value);
        }
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        } else {
            return (obj != null && this.getClass().equals(obj.getClass())) &&
                    this.value.equals(((Enum) obj).value);
        }
    }

    @Override
    public int hashCode() {
        return this.getClass().hashCode() ^ this.value.hashCode();
    }

    @Override
    public String toString() {
        return this.getName();
    }

    protected Object writeReplace() throws ObjectStreamException {
        this.getName();
        return this;
    }

    protected Object readResolve() throws ObjectStreamException {

        Class<?> enumClass = this.ensureClassLoaded();
        EnumType enumType = EnumUtil.getEnumType(enumClass);
        Enum enumObject = enumType.nameMap.get(this.getName());
        if (enumObject == null) {
            enumType.populateNames(enumClass);
            enumObject = enumType.nameMap.get(this.getName());
        }

        if (enumObject == null) {
            throw new InvalidClassException("Enum name \"" +
                    this.getName() + "\" not found in class " + enumClass.getName());
        } else if (!enumObject.value.equals(this.value)) {
            throw new InvalidClassException("Enum value \"" +
                    this.value + "\" does not match in class " + enumClass.getName());
        } else {
            return enumObject;
        }

    }

    private Class<?> ensureClassLoaded() {

        Class<?> enumClass = this.getClass();
        synchronized (this) {
            return enumClass;
        }
    }

    private Enum setName(String name) {
        if (this.name != null) {
            throw new IllegalStateException("Enum name already set: " + this.name);
        } else {
            this.name = name;
            return this;
        }
    }

    protected static Enum create() {
        return createEnum(null, null, false);
    }

    protected static Enum create(String name) {
        return createEnum(name, null, false);
    }

    static Enum createEnum(Number value) {
        return createEnum(null, value, true);
    }

    static Enum createEnum(String name, Number value) {
        return createEnum(name, value, true);
    }

    private static Enum createEnum(String name, Number value, boolean withValue) {

        String enumClassName = null;
        Class<?> enumClass;
        Enum enumObject;
        try {
            enumClassName = getCallerClassName();
            enumClass = ClassLoaderUtil.loadClass(enumClassName);
            enumObject = (Enum) enumClass.newInstance();
            enumObject.setName(StringUtil.trimToNull(name));
        } catch (ClassNotFoundException e) {
            throw new CreateEnumException("Could not find enum class " + enumClassName, e);
        } catch (InstantiationException | IllegalAccessException e) {
            throw new CreateEnumException("Could not instantiate enum instance of class "
                    + enumClassName, e);
        }

        if (withValue && value == null) {
            throw new NullPointerException("The Enum value must not be null");
        } else {
            EnumType enumType = EnumUtil.getEnumType(enumClass);
            boolean flagMode = enumObject instanceof Flags;
            if (withValue) {
                enumObject.value = enumType.setValue(value, flagMode);
            } else {
                enumObject.value = enumType.getNextValue(flagMode);
            }

            enumType.enumList.add(enumObject);
            if (!enumType.valueMap.containsKey(enumObject.value)) {
                enumType.valueMap.put(enumObject.value, enumObject);
            }

            if (enumObject.name != null && !enumType.nameMap.containsKey(enumObject.name)) {
                enumType.nameMap.put(enumObject.name, enumObject);
            }

            return enumObject;
        }
    }

    private static String getCallerClassName() {

        StackTraceElement[] callers = new Throwable().getStackTrace();
        String enumClass = Enum.class.getName();

        for (StackTraceElement caller : callers) {
            String className = caller.getClassName();
            String methodName = caller.getMethodName();
            if (!enumClass.equals(className) && "<clinit>".equals(methodName)) {
                return className;
            }
        }
        throw new CreateEnumException("Cannot get Enum-class name");
    }

    protected abstract static class EnumType {

        private Number value;
        final Map<String, Enum> nameMap = Collections.synchronizedMap(new HashMap<>());
        final Map<Number, Enum> valueMap = Collections.synchronizedMap(new HashMap<>());
        final List<Enum> enumList = new ArrayList<>();

        protected EnumType() {
        }

        final Number setValue(Number value, boolean flagMode) {
            this.value = value;
            return value;
        }

        final Number getNextValue(boolean flagMode) {
            this.value = this.getNextValue(this.value, flagMode);
            if (flagMode && this.isZero(this.value)) {
                throw new UnsupportedOperationException("The flag value is out of range");
            } else {
                return this.value;
            }
        }

        final void populateNames(Class<?> enumClass) {
            synchronized (this) {
                Field[] fields = enumClass.getFields();

                for (Field field : fields) {
                    int modifier = field.getModifiers();
                    if (Modifier.isPublic(modifier) && Modifier.isFinal(modifier) && Modifier.isStatic(modifier)) {
                        try {
                            Object value = field.get(null);

                            for (Enum enumObject : this.valueMap.values()) {
                                if (value == enumObject && enumObject.name == null) {
                                    enumObject.name = field.getName();
                                    this.nameMap.put(enumObject.name, enumObject);
                                    break;
                                }
                            }
                        } catch (IllegalAccessException e) {
                            throw new CreateEnumException(e);
                        }
                    }
                }

            }
        }

        protected abstract Class<?> getUnderlyingClass();

        protected abstract Number getNextValue(Number value, boolean flagMode);

        protected abstract boolean isZero(Number value);

    }

    //TODO
}
